﻿using System.Collections.Concurrent;
using ClassProxitator.Messages;
using System;
using System.Net.Sockets;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ClassProxitator
{
	public class StreamConnection: IConnection
    {
		public static StreamConnection ConnectTo(string hostName, int port)
        {
            var client = new TcpClient();
            client.NoDelay = true;
            client.Connect(hostName, port);
            return new StreamConnection(client.GetStream());
        }
		
		public IList<IConnection> Connections
		{
			get{
				return this._connections;
			}
		}
		
        readonly Stream _readStream;
        readonly Stream _writeStream;
        List<IConnection> _connections = new List<IConnection>();
        ConcurrentDictionary<int, TaskCompletionSource<object>> Tasks = new ConcurrentDictionary<int, TaskCompletionSource<object>>();
        ConcurrentQueue<Message> MessageQueue = new ConcurrentQueue<Message>();
        int taskCounter = 0;
        int writing = 0;
        Task readTask;
        
        public StreamConnection(Stream read, Stream write)
        {
            if (!read.CanRead) throw new ArgumentException("read Stream can't read");
            if (!write.CanWrite) throw new ArgumentException("write Stream can't write");

            this._readStream = read;
            this._writeStream = write;
            
            this.readTask = this.Read();
        }
        
        public StreamConnection(Stream readWrite)
        {
        	if (!readWrite.CanRead) throw new ArgumentException("Stream can't read");
            if (!readWrite.CanWrite) throw new ArgumentException("Stream can't write");
            this._readStream = this._writeStream = readWrite;
            
            this.readTask = this.Read();
        }

        async Task Read()
        {
            while(_readStream.CanRead)
            {
                var msg = await Message.Load(_readStream).ConfigureAwait(false);
                ProcessMessage(msg);
            }
        }

        void ProcessMessage(Message msg)
        {
        	switch(msg.MessageType)
        	{
        		case MessageType.MethodCall:
        			ProcessMethodCall(msg as MethodCallMessage);
        			break;
        		case MessageType.Result:
        			ProcessResult(msg as ResultMessage);
        			break;
        		case MessageType.Exception:
        			ProcessException(msg as ExceptionMessage);
        			break;
        		default:
        			throw new NotSupportedException("message type not known");
        	}
        }

        void ProcessMethodCall(MethodCallMessage msg)
        {
            if(_connections.Count < msg.InstanceId || msg.InstanceId < 0)
            {
                Throw(new IndexOutOfRangeException("InstanceId"), msg.TaskId);
                return;
            }
            var instance = _connections[msg.InstanceId];
            try
            {
                instance.CallMethod(msg.MethodName, msg.Parameters).ContinueWith(t =>
                {
                    if (t.Exception != null)
                        Throw(t.Exception, msg.TaskId);
                    else Return(t.Result, msg.TaskId);
                });
            }
            catch(Exception e)
            {
                Throw(e, msg.TaskId);
            }
        }
        
        void ProcessResult(ResultMessage msg)
        {
        	TaskCompletionSource<object> tcs;
        	if(this.Tasks.TryGetValue(msg.TaskId, out tcs))
        	{
        		tcs.TrySetResult(msg.Data);
        	}
        }
        
        void ProcessException(ExceptionMessage msg)
        {
        	TaskCompletionSource<object> tcs;
        	if(this.Tasks.TryGetValue(msg.TaskId, out tcs))
        	{
        		tcs.TrySetException(msg.Exception);
        	}
        }

        /// <summary>
        /// Adds return message to queue
        /// </summary>
        void Return(object data, int taskId)
        {
        	this.SendMessage(new ResultMessage{
        	                 	Data = data,
        	                 	TaskId = taskId
        	                 });
        }

        /// <summary>
        /// adds exception message to queue
        /// </summary>
        void Throw(Exception e, int taskId)
        {
        	this.SendMessage(new ExceptionMessage{
        	                 	Exception = e,
        	                 	TaskId=taskId
        	                 });
        }
        
        /// <summary>
        /// adds message to queue and starts writing
        /// </summary>
        void SendMessage(Message msg)
        {
        	this.MessageQueue.Enqueue(msg);
        	this.Writing();
        }
        
        public Task<object> CallMessage(string name, object[] parameters, int instanceId = 0)
        {
        	var taskId = Interlocked.Increment(ref this.taskCounter);
        	var msg = new MethodCallMessage{
        		InstanceId = instanceId,
        		MethodName = name,
        		Parameters = parameters,
        		TaskId = taskId
        	};
        	this.SendMessage(msg);
        	var tcs = new TaskCompletionSource<object>();
        	this.Tasks.TryAdd(taskId, tcs);
        	return tcs.Task;
        }
        
		public Task<object> CallMethod(string name, params object[] parameters)
		{
			return CallMessage(name, parameters, 0);
		}
		
        /// <summary>
        /// Starts writing to output stream from message queue
        /// if already running, returns
        /// non blocking operation
        /// </summary>
        async void Writing()
        {
        	if(Interlocked.Exchange(ref writing, 1) == 1)
        		return;
        	Message a;
        	try{
        		while(this.MessageQueue.TryDequeue(out a))
        		{
        			await a.WriteTo(_writeStream);
        		}
        	}
        	finally{
        		Interlocked.Exchange(ref this.writing, 0);
        		if(this.MessageQueue.TryPeek(out a)) this.Writing(); // to not cause latencies and even deadlocks
        	}
        }
    }
}
